package com.alogic.auth.client;

import com.alogic.auth.*;
import com.alogic.remote.util.HttpQuery;
import com.alogic.xscript.Logiclet;
import com.alogic.xscript.LogicletContext;
import com.alogic.xscript.Script;
import com.alogic.xscript.doc.XsObject;
import com.alogic.xscript.doc.json.JsonObject;
import com.anysoft.util.*;
import com.logicbus.backend.Context;
import com.logicbus.backend.message.JsonMessage;
import com.logicbus.backend.server.http.HttpContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 缺省的客户端实现
 * @since 1.6.12.24 [20190311 duanyy] <br>
 *
 * @version 1.6.14.13 [20210708 duanyy] <br>
 * - 优化callback路径的拼接逻辑 <br>
 */
public class Default extends AuthenticationHandler.Abstract implements Constants{
    /**
     * 会话管理器
     */
    protected SessionManager sessionManager = null;

    /**
     * 当注销时执行脚本
     */
    protected Logiclet onLogout = null;

    /**
     * 扩展指令时执行脚本
     */
    protected Logiclet onCommand = null;

    /**
     * 验证菜单权限的脚本
     */
    protected Script onMenu = null;

    /**
     * 进行token验证
     */
    protected Script onValidate = null;

    /**
     * 当Principal输出时的脚本
     */
    protected Logiclet onPrincipal = null;

    /**
     * 参数token的参数id
     */
    protected String arguToken = "token";

    protected String callbackPath = "/logout";

    /**
     * 服务器的token验证地址
     */
    protected String validateURL = "${auth.server}/services/auth/GetToken";

    protected String arguCallback = "callback";

    protected String tokenCookie = "";
    protected String dftApp = "";
    /**
     * 编码
     */
    protected String encoding = "utf-8";

    @Override
    public boolean isLocalLoginMode(){
        return false;
    }

    @Override
    public void configure(Element e, Properties p) {
        Properties props = new XmlElementProperties(e,p);

        Element elem = XmlTools.getFirstElementByPath(e, "on-logout");
        if (elem != null){
            onLogout = Script.create(elem, props);
        }

        elem = XmlTools.getFirstElementByPath(e, "on-command");
        if (elem != null){
            onCommand = Script.create(elem, props);
        }

        elem = XmlTools.getFirstElementByPath(e, "on-menu");
        if (elem != null){
            onMenu = Script.create(elem, props);
        }

        elem = XmlTools.getFirstElementByPath(e, "on-validate");
        if (elem != null){
            onValidate = Script.create(elem, props);
        }

        elem = XmlTools.getFirstElementByPath(e, "on-principal");
        if (elem != null){
            onPrincipal = Script.create(elem, props);
        }

        configure(props);
    }

    @Override
    public void configure(Properties p){
        super.configure(p);
        validateURL = PropertiesConstants.getString(p, "auth.server.validate", validateURL);
        arguToken = PropertiesConstants.getString(p,"auth.para.token",arguToken);
        callbackPath = PropertiesConstants.getString(p,"auth.logout.callback",callbackPath);
        arguCallback = PropertiesConstants.getString(p, "auth.para.callback", arguCallback);
        tokenCookie = PropertiesConstants.getString(p, "tokenFromCookie", tokenCookie);
        dftApp = PropertiesConstants.getString(p,"dftApp",dftApp,true);
        encoding = PropertiesConstants.getString(p,"http.encoding",encoding);
    }

    @Override
    public Principal getCurrent(HttpServletRequest request,HttpServletResponse response) {
        Session sess = sessionManager.getSession(request,response, true);
        return getCurrent(request,response,sess);
    }

    @Override
    public synchronized Principal getCurrent(HttpServletRequest request,HttpServletResponse response,Session session) {
        Session sess = session;
        if (sess == null){
            //保证session不为空
            sess = sessionManager.getSession(request,response, true);
        }

        if (sess.isLoggedIn()){
            //已经登录
            String token = sess.hGet(Session.DEFAULT_GROUP, Session.TOKEN, "");
            return new ThePrincipal(token,sess,this.onPrincipal,this.dftApp);
        }
        String token = request.getParameter(arguToken);
        if (StringUtils.isNotEmpty(token)){
            //参数中指定了新的token
            String oldToken = sess.hGet(Session.DEFAULT_GROUP, Session.TOKEN, "");
            if (token.equals(oldToken)){
                //和以前的token一致
                if (sess.isLoggedIn()){
                    return new ThePrincipal(token,sess,this.onPrincipal,this.dftApp);
                }
            }else{
                //新的token，删除前一个token的用户信息和权限信息
                sess.hDel(Session.USER_GROUP);
                sess.sDel(Session.PRIVILEGE_GROUP);
                //记录token到当前的session
                sess.hSet(Session.DEFAULT_GROUP, Session.TOKEN, token, true);
            }
        }else{
            token = sess.hGet(Session.DEFAULT_GROUP, Session.TOKEN, "");
        }

        if (StringUtils.isEmpty(token) && StringUtils.isNotEmpty(tokenCookie)){
            token = sessionManager.getCookie(request, tokenCookie, "");
        }

        if (StringUtils.isNotEmpty(token) && onValidate != null){
            Context ctx = new HttpContext(request,response,encoding);
            LogicletContext logicletContext = new Context.ServantLogicletContext(ctx);
            CookieManager cm = new CookieManager.Default(sessionManager, request,response);
            try {
                String callback = getCallbackURL(request,sess.getId());
                String clientIp = httpClientTool.getClientIp(request);

                HttpQuery query = new HttpQuery(validateURL);
                query.param(this.arguToken, token);
                query.param(this.arguCallback, callback);
                query.param("fromIp",clientIp);

                String validateEndpoint = query.toString();

                logicletContext.setObject(ID_SESSION, sess);
                logicletContext.setObject(ID_COOKIES, cm) ;
                logicletContext.SetValue("$service", "/auth/Validate");
                logicletContext.SetValue("$clientIp",clientIp);
                logicletContext.SetValue("$callbackUrl",callback);
                logicletContext.SetValue("$token", token);
                logicletContext.SetValue("$validate-url", validateURL);
                logicletContext.SetValue("$validate-endpoint",validateEndpoint);
                XsObject doc = new JsonObject("root",new HashMap<String,Object>());
                onValidate.execute(doc,doc, logicletContext, null);
            }finally{
                logicletContext.removeObject(ID_SESSION);
                logicletContext.removeObject(ID_COOKIES);
            }
        }
        return session.isLoggedIn()?new ThePrincipal(token,sess,this.onPrincipal,this.dftApp):null;
    }

    protected String getCallbackURL(HttpServletRequest request,String callbackId){
        StringBuffer callbackURL = new StringBuffer();
        callbackURL.append(httpClientTool.getContextBase(request)).append(callbackPath).append("?callback=" + callbackId);
        return callbackURL.toString();
    }

    @Override
    public Principal getPrincipal(String app, String token,String callback) {
        throw new BaseException("core.e1000","In sso client mode,it's not supported to get principal by token.");
    }

    @Override
    public Principal login(HttpServletRequest request,HttpServletResponse response) {
        throw new BaseException("core.e1000","In sso client mode,it's not supported to login.");
    }

    @Override
    public void setSessionManager(SessionManager sm) {
        this.sessionManager = sm;
    }

    @Override
    public void logout(HttpServletRequest request,HttpServletResponse response) {
        Context ctx = new HttpContext(request,response,encoding);
        logout(ctx);
    }

    @Override
    public void logout(Context ctx){
        if (onLogout == null){
            throw new BaseException("core.e1000","This function is not supported.");
        }

        HttpContext httpContext = (HttpContext)ctx;
        HttpServletRequest request = httpContext.getRequest();
        HttpServletResponse response = httpContext.getResponse();

        CookieManager cm = new CookieManager.Default(this.sessionManager, request, response);
        Session sess = getSession(sessionManager,request,response, true);
        LogicletContext logicletContext = new Context.ServantLogicletContext(ctx);
        try {
            Principal principal =  new ThePrincipal(sess.getId(),sess,this.onPrincipal,this.dftApp);
            logicletContext.setObject(Constants.ID_PRINCIPAL, principal);
            logicletContext.setObject(Constants.ID_SESSION, sess);
            logicletContext.setObject(Constants.ID_COOKIES, cm) ;
            logicletContext.SetValue("$service", "/auth/Logout");
            logicletContext.SetValue("$clientIp",getClientIp(request));
            XsObject doc = new JsonObject("root",new HashMap<String,Object>());
            onLogout.execute(doc,doc, logicletContext, null);
        }finally{
            logicletContext.removeObject(Constants.ID_PRINCIPAL);
            logicletContext.removeObject(Constants.ID_SESSION);
            logicletContext.removeObject(Constants.ID_COOKIES);
        }
    }

    @Override
    public void checkPrivilege(Principal principal,Map<String,Object> menu){
        if (onMenu != null && menu != null){
            LogicletContext logicletContext = new LogicletContext(Settings.get());
            try {
                logicletContext.setObject(Constants.ID_PRINCIPAL, principal);
                XsObject doc = new JsonObject("root",menu);
                onMenu.execute(doc,doc, logicletContext, null);
            }finally{
                logicletContext.removeObject(Constants.ID_PRINCIPAL);
            }
        }
    }

    @Override
    public void command(Context ctx){
        JsonMessage msg = (JsonMessage)ctx.asMessage(JsonMessage.class);
        Map<String,Object> data = new HashMap<String,Object>();
        if (onCommand != null){
            HttpContext httpContext = (HttpContext)ctx;
            HttpServletRequest request = httpContext.getRequest();
            HttpServletResponse response = httpContext.getResponse();

            CookieManager cm = new CookieManager.Default(this.sessionManager, request, response);
            Session sess = getSession(sessionManager,request,response, true);
            LogicletContext logicletContext = new Context.ServantLogicletContext(ctx);
            try {
                Principal principal =  new ThePrincipal(sess.getId(),sess,this.onPrincipal,this.dftApp);
                logicletContext.setObject(Constants.ID_PRINCIPAL, principal);
                logicletContext.setObject(Constants.ID_SESSION, sess);
                logicletContext.setObject(Constants.ID_COOKIES, cm) ;
                logicletContext.SetValue("$service", "/auth/Command");
                logicletContext.SetValue("$clientIp",getClientIp(request));
                XsObject doc = new JsonObject("root",data);
                onCommand.execute(doc,doc, logicletContext, null);
            }finally{
                logicletContext.removeObject(Constants.ID_PRINCIPAL);
                logicletContext.removeObject(Constants.ID_SESSION);
                logicletContext.removeObject(Constants.ID_COOKIES);
            }
        }
        msg.getRoot().put("data", data);
    }

    protected Session getSession(SessionManager sm,HttpServletRequest request,HttpServletResponse response,boolean create){
        return sm.getSession(request,response,create);
    }

    /**
     * principal实现
     * @author yyduan
     *
     */
    public static class ThePrincipal extends SessionPrincipal{
        /**
         * a logger of slf4j
         */
        protected final Logger LOG = LoggerFactory.getLogger(com.alogic.auth.local.Default.ThePrincipal.class);

        /**
         * 输出时的脚本
         */
        protected Logiclet onReport = null;

        public ThePrincipal(String id, Session session,Logiclet onReport,String app) {
            super(id, session,app);
            this.onReport = onReport;
        }

        @Override
        public void report(Map<String, Object> json) {
            if (json != null){
                if (onReport == null){
                    JsonTools.setString(json, "id", getId());

                    List<Pair<String,String>> entries = this.session.hGetAll(USER_GROUP,"*");
                    if (entries != null){
                        Map<String,Object> map = new HashMap<String,Object>();
                        for (Pair<String,String> p:entries){
                            JsonTools.setString(map,p.key(), p.value());
                        }
                        json.put("property", map);
                    }

                    List<String> privileges = this.getPrivileges();
                    if (privileges != null && !privileges.isEmpty()){
                        json.put("privilege", privileges);
                    }
                }else{
                    LogicletContext logicletContext = new LogicletContext(Settings.get());
                    try {
                        logicletContext.setObject(Constants.ID_PRINCIPAL, this);
                        XsObject doc = new JsonObject("root",json);
                        onReport.execute(doc,doc, logicletContext, null);
                    }catch (Exception ex){
                        LOG.info("Failed to execute script:" + ExceptionUtils.getStackTrace(ex));
                    }finally{
                        logicletContext.removeObject(Constants.ID_PRINCIPAL);
                    }
                }
            }
        }
    }
}